/*
 * Copyright (C) 2009 Yury Kudryashov.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package name.kudryashov.base;

import name.kudryashov.functions.Generator;
import name.kudryashov.annotations.Immutable;

import java.io.Serializable;
import java.util.NoSuchElementException;

/**
 * Object that either has some value or does not.
 * <p>
 * @param <T> type of the value
 */
// TODO implement list operations - filter, map, size, get(index), flatMap
@Immutable
public abstract class Option<T> implements Serializable, Generator<T> {

    /**
     * @return <code>true</code> if this is none
     */
    public abstract boolean isEmpty();

    /**
     * @return <code>true</code> if this is <code>none</code>
     */
    public final boolean isNone() { return isEmpty(); }

    /**
     * @return <code>true</code> if this is <code>some</code>
     */
    public final boolean isDefined() { return !isEmpty(); }

    /**
     * @return <code>true</code> if this is <code>some</code>
     */
    public final boolean isSome() { return !isEmpty(); }


    /**
     * @return the value
     * @throws NoSuchElementException if this is None
     */
    public abstract T get() throws NoSuchElementException;

    /**
     * If this is Some, return the value or return a default value.
     * @param other default value to return if this is None
     * @return the value (if this is Some) or the given value (if this is None)  
     */
    public final T getOrElse(T other) {
        return isDefined() ? get() : other;
    }

    /**
     * If this is Some, return the value or return a default value produced by the given generator.
     * @param other generator to produce a default value to return if this is None
     * @return the value (if this is Some) or the value produced by the given generator (if this is None)
     * @throws NullPointerException this is None and other is null
     */
    public final T getOrElse(Generator<T> other) {
        return isDefined() ? get() : other.get();
    }

    /**
     * If this is Some, return the value or return null.
     * @return the value (if this is Some) or null (if this is None)  
     */
    public final T getOrNull() {
        return getOrElse((T) null);
    }

    /**
     * If this is Some return a value, otherwise throw an exception
     * @param e exception to throw, not null
     * @param <E> type of exception
     * @return the value (if this is Some) or throws an exception (if this is None)
     * @throws E the given exception
     * @throws NullPointerException this is None and e is null
     */
    @SuppressWarnings({"ThrowableResultOfMethodCallIgnored"})
    public final <E extends Throwable> T getOrThrow(E e) throws E {
        if(isDefined()) {
            return get();
        }
        throw e;
    }

    /**
     * If this is Some return a value, otherwise throw an new instance of an exception specified by the given class.
     * @param eClass class of the exception to throw, not null. Exception should have a default constructor.
     * @param <E> type of exception
     * @return the value (if this is Some) or throws an exception (if this is None)
     * @throws E the exception
     * @throws NullPointerException this is None and eClass is null
     * @throws IllegalAccessException if there's no access to the specified exception
     * @throws InstantiationException there was an exception instantianting an exception
     */
    @SuppressWarnings({"ThrowableResultOfMethodCallIgnored"})
    public final <E extends Throwable> T getOrThrow(Class<E> eClass) throws E, IllegalAccessException, InstantiationException {
        if(isDefined()) {
            return get();
        }
        throw eClass.newInstance();
    }

    /**
     * If this is Some return a value, otherwise throw an new instance of an exception returned by the given generator.
     * @param f function that will generate an exception to throw.
     * @param <E> type of exception
     * @return the value (if this is Some) or throws an exception (if this is None)
     * @throws E the exception
     * @throws NullPointerException this is None and generator is null
     */
    @SuppressWarnings({"ThrowableResultOfMethodCallIgnored"})
    public final <E extends Throwable> T getOrThrow(Generator<E> f) throws E {
        if(isDefined()) {
            return get();
        }
        throw f.get();
    }

    /**
     * Constructs a new {@link Some} with the given value
     * @param x value
     * @param <T> type of value
     * @return new {@link Some} value
     */
    public static <T> Option<T> some(T x) {
        return Some.some(x);
    }

    /**
     * @return new {@link None} value
     */
    public static <T> Option<T> none() {
        return None.none();
    }

    private static final long serialVersionUID = -4967844467537396310L;
}
